/*Machine is a class that encapsulates methods for generating a timing diagram for a given program,
loading the program, and tracking D-u and register activity */

#include "Machine.h"

namespace TimingDiagram
{

    Machine::Machine(int numDUnits, int numRegisters, int addTime, int mulTime, int iuTime){
        _numDUnits = numDUnits;
        _numRegisters = numRegisters;
        _dUnits = new DUnit[numDUnits];
        _registers = new Register[numRegisters];

        _addTime = addTime;
        _mulTime = mulTime;
        _iuTime = iuTime;

        _loaded = false;

        for(int i=0; i<numDUnits; i++){
            _dUnits[i].first.second = -1;
        }

        for(int i=0; i<numRegisters; i++){
            _registers[i] = false;
        }
    }

    Machine::~Machine(){
        delete [] _dUnits;
        delete [] _registers;
        _instructions.clear();
    }

    //Method for loading the program from a file
    void Machine::loadProgram(std::string fileName){
        std::ifstream fin;
        fin.open(fileName.c_str(), std::ios::in);

        _fileName = fileName;

        if(fin.is_open()){
            std::string line;
            size_t lineNum = 0;
            while(std::getline(fin, line)){
                std::stringstream ss(line);
                std::string op;
                int dest, src1, src2;

                ss >> op >> dest >> src1 >> src2;
                _program << lineNum << " : " << line << std::endl;

                OPERATOR opSwitch = OP_UNKNOWN;
                if(op.compare("ADD") == 0)
                    opSwitch = OP_ADD;
                else if(op.compare("MUL") == 0)
                    opSwitch = OP_MUL;

                Instruction inst;
                inst.op = opSwitch;
                inst.dest = dest;
                inst.src1 = src1;
                inst.src2 = src2;
                _instructions.push_back(inst);

                lineNum++;
            }

            _loaded = true;
        }
        else
            std::cout << "Error opening program file." << std::endl;

        fin.close();

        computeInstructionDependencies();
    }

    //Main method for generating the timing diagram.
    void Machine::outputDiagram(){
        if(_loaded){
            std::stringstream ss;

            if(getNumberOfRegistersUsed() <= _numRegisters){
                for(size_t i=0; i<_instructions.size(); i++){
                    Instruction inst = _instructions[i];

                    _registers[inst.dest] = true;
                    _registers[inst.src1] = true;
                    _registers[inst.src2] = true;

                    //If an instruction is dependent on more than one other instruction, we only need to wait for the longest instruction to finish
                    int longestDependencyDelay = 0;
                    for(size_t j=0; j<inst.dependencies.size(); j++){
                            if(_instructions[inst.dependencies[j]].endCycle > longestDependencyDelay)
                                longestDependencyDelay = _instructions[inst.dependencies[j]].endCycle;
                    }

                    int lastRunTime = 0; //Tracks the total cycles used by the instruction previously in a D-unit

                    int nextUnit = getNextFreeDUnit(); //Gets the next D-unit to use

                    if(nextUnit != -1){ //There is a free d-u
                        _dUnits[nextUnit].first.first = i*_iuTime; //If there is a free D-u, we can still issue the instruction

                        if(longestDependencyDelay != 0){ //There is an instruction dependency
                            _dUnits[nextUnit].second = longestDependencyDelay-_dUnits[nextUnit].first.first;
                        }

                        //Update the D-u with the new instruction's run time
                        if(inst.op == OP_ADD)
                            _dUnits[nextUnit].first.second = _addTime;
                        else if(inst.op == OP_MUL)
                            _dUnits[nextUnit].first.second = _mulTime;
                    }
                    else{ //All dunits are in use
                        nextUnit = getDUnitAvailableNext();
                        lastRunTime = _dUnits[nextUnit].first.first+_dUnits[nextUnit].first.second+_dUnits[nextUnit].second;

                        _dUnits[nextUnit].first.first = lastRunTime+_iuTime; //Must wait for the next available d-u before issue

                        if(longestDependencyDelay != 0){ //There is an instruction dependency
                            if(lastRunTime < longestDependencyDelay) //If the last instruction in the D-u ends before we're allowed to execute, delay execution
                                _dUnits[nextUnit].second = longestDependencyDelay-_dUnits[nextUnit].first.first;
                            else //Otherwise, we don't need to delay
                                _dUnits[nextUnit].second = 0;
                        }

                        //Update the D-u with the new instruction's run time
                        if(inst.op == OP_ADD)
                            _dUnits[nextUnit].first.second = _addTime;
                        else if(inst.op == OP_MUL)
                            _dUnits[nextUnit].first.second = _mulTime;
                    }

                    //Store the start and end cycles with the instruction, so that it can be used to calculate dependency information for a
                    //subsequent instruction
                    _instructions[i].startCycle = _dUnits[nextUnit].first.first;
                    _instructions[i].endCycle = _dUnits[nextUnit].first.first+_dUnits[nextUnit].first.second+_dUnits[nextUnit].second;

                    //With all delays and execution times determined, print this instructions line on the diagram
                    if(i == (_instructions.size()/2)){
                        ss << "Instructions  ";
                    }
                    else
                        ss << "              ";

                    ss << "Inst " << i << ": |";
                    for(int j=0; j<_instructions[i].startCycle; j++)
                        ss << "_|";
                    for(int j=0; j<_iuTime; j++)
                        ss << "I|";
                    for(int j=0; j<_dUnits[nextUnit].second; j++)
                        ss << "_|";
                    if(inst.op == OP_ADD){
                        for(int j=0; j<_addTime; j++)
                            ss << "+|";
                    }
                    else if(inst.op == OP_MUL){
                        for(int j=0; j<_mulTime; j++)
                            ss << "*|";
                    }
                    ss << std::endl;

                    //Determine the character width of the diagram by checking the length of the final line
                    if(i == _instructions.size()-1){
                        _lastLineLength = _instructions[i].startCycle+_iuTime+_dUnits[nextUnit].second;
                        if(inst.op == OP_ADD)
                            _lastLineLength += _addTime;
                        else if(inst.op == OP_MUL)
                            _lastLineLength += _mulTime;
                    }

                }


                //Output the program we're running
                std::cout << "Program Input: " << std::endl;
                std::cout << _program.str() << std::endl;

                //Output diagram with title and x-axis label
                std::cout << "\t              Timing Diagram for Program " << _fileName << std::endl;
                std::cout << std::endl << ss.str() << std::endl; //Print the graph to standard output

                //Attempt to center the x-axis label
                for(int i=0; i<_lastLineLength; i++)
                    std::cout << " ";
                std::cout << "              Time (cycles)" << std::endl << std::endl;

            }
            else
                std::cout << "Not enough registers to execute program." << std::endl;
        }
        else
            std::cout << "Machine::outputDiagram(): program not loaded." << std::endl;
    }

    //Determines the index of the next free D-u
    //Returns -1 if all D-units are in use
    //Once current instruction number > total D-units, this will always return -1 from the current instruction forward
    int Machine::getNextFreeDUnit(){
        for(int i=0; i<_numDUnits; i++){
            if(_dUnits[i].first.second == -1)
                return i;
        }

        return -1;
    }

    //Determines the index of the D-u that will be next available
    //Does this by checking each D-u to see which will terminate first
    int Machine::getDUnitAvailableNext(){
        int min = _dUnits[0].first.first+_dUnits[0].first.second+_dUnits[0].second;
        int minIdx = 0;
        for(int i=1; i<_numDUnits; i++){
            if(_dUnits[i].first.first+_dUnits[i].first.second+_dUnits[i].second < min){
                min = _dUnits[i].first.first+_dUnits[i].first.second+_dUnits[i].second;
                minIdx = i;
            }
        }

        return minIdx;
    }

    //Method which iterates over all the instructions, and determines the dependenices between them.
    //For a given instruction i, all instructions (indices) that it depends on will be stored in a vector belonging to i.
    //Since the diagram will take a scoreboard into account, we only need to worry about OI dependencies
    void Machine::computeInstructionDependencies(){
        std::cout << std::endl << "Dependencies: " << std::endl;
        for(size_t i=0; i<_instructions.size(); i++){
            for(size_t j=i+1; j<_instructions.size(); j++){
                //OI dependency
                if((_instructions[i].dest == _instructions[j].src1) || (_instructions[i].dest == _instructions[j].src2)){
                    _instructions[j].dependencies.push_back(i);
                    std::cout << "Instruction " << j << " depends on instruction " << i << std::endl;
                }
            }
        }
        std::cout << std::endl;
    }

    //Method to determine the total number of registers that will be used by the input program
    int Machine::getNumberOfRegistersUsed(){
        std::vector<int> registers;
        std::vector<int>::iterator it;

        for(size_t i=0; i<_instructions.size(); i++){
            int d = _instructions[i].dest;
            int s = _instructions[i].src1;
            int r = _instructions[i].src2;

            it = std::find(registers.begin(), registers.end(), d);
            if(it == registers.end())
                registers.push_back(d);

            it = std::find(registers.begin(), registers.end(), s);
            if(it == registers.end())
                registers.push_back(s);

            it = std::find(registers.begin(), registers.end(), r);
            if(it == registers.end())
                registers.push_back(r);
        }

        return registers.size();
    }

}
